package huadi.sso.client.filter;

import static huadi.sso.client.SSOConstants.IS_LOGINED;
import static huadi.sso.client.SSOConstants.LOGOUT_SERVER_TICKET;
import static huadi.sso.client.SSOConstants.PARAM_NAME_CLIENT_IDENTIFIER;
import static huadi.sso.client.SSOConstants.PARAM_NAME_REQUEST_URL;
import static huadi.sso.client.SSOConstants.PARAM_NAME_VALIDATE_REQUEST;
import static huadi.sso.client.SSOConstants.SERVER_TICKET_NAME;
import static huadi.sso.client.SSOConstants.SESSION_ATTR_NAME_USERNAME;
import static huadi.sso.client.SSOConstants.TICKET_PARAM_NAME;
import huadi.sso.client.PassportJsonConstants;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import net.sf.json.JSONObject;
import net.sf.json.util.JSONUtils;

/**
 * SSO client filter.
 * 
 * @author Huadi
 */
public class AuthenticationFilter implements Filter {

	/**
	 * The sign in URL for SSO client. This var is used to send a sign in
	 * request.
	 */
	private String clientSignInUrl;

	/**
	 * The URL to validate a ServerTicket. The filter will send the ST to this
	 * URL to validate it.
	 */
	private String validateServerTicketUrl;

	/**
	 * The URL to the client server. This var is used to a parameter on redirect
	 * and cookie path.
	 */
	private String serverUrl;

	/**
	 * Save the redirect URL after signing in. This var is used to a parameter
	 * on redirect.
	 */
	private String requestUrl;

	/*
	 * The sessionIdToSessionMapping map and the serverTicketToSessionIdMapping
	 * map make a ServerTicket-to-HttpSession mapping. The 2 reasons that make
	 * this mapping use 2 maps are: First, we must delete the references to a
	 * HttpSession object so the GC thread can recycle the memory, so we must
	 * maintain a mapping. Second, it can only get the sessionId when a session
	 * is destroyed in the web container, so we can only get the reference to a
	 * session by sessionId.
	 */
	private static Map<String, HttpSession> sessionIdToSessionMapping = new ConcurrentHashMap<String, HttpSession>();

	private Map<String, String> serverTicketToSessionIdMapping = new ConcurrentHashMap<String, String>();

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {

		HttpServletRequest httpRequest = (HttpServletRequest) request;
		HttpServletResponse httpResponse = (HttpServletResponse) response;
		String param = this.getParams(httpRequest);
		this.requestUrl = param.equals("") ? httpRequest.getRequestURL()
				.toString() : httpRequest.getRequestURL().toString() + "?"
				+ param;

		// if the request method is not "POST" redirect to error page.
		if (!httpRequest.getMethod().toUpperCase().equals("POST"))
			httpResponse.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);

		// check if the request is a sign out request
		// if so, remove the session from the mapping.
		String logoutServerTicket = httpRequest
				.getParameter(LOGOUT_SERVER_TICKET);
		if (logoutServerTicket != null) {
			final String sessionId = this.serverTicketToSessionIdMapping
					.remove(logoutServerTicket);
			if (sessionId != null) {
				sessionIdToSessionMapping.remove(sessionId);
			}
			return;
		}

		// Check if the user has signed in.
		String st = this.getServerTicketFromCookie(httpRequest);
		HttpSession httpSession = null;
		if (st != null) {
			final String sessionId = this.serverTicketToSessionIdMapping
					.get(st);
			if (sessionId != null) {
				httpSession = AuthenticationFilter.sessionIdToSessionMapping
						.get(sessionId);
				if (httpSession != null) {
					Boolean isLogined = (Boolean) httpSession
							.getAttribute(IS_LOGINED);
					if (isLogined != null && isLogined) {
						chain.doFilter(httpRequest, httpResponse);
						return;
					}
				} else {
					this.serverTicketToSessionIdMapping.remove(st);
					this.removeServerTicketCookie(httpRequest, httpResponse);
				}
			} else {
				this.removeServerTicketCookie(httpRequest, httpResponse);
			}
		}

		// if not signed in, validate the ST if exist.
		String ticket = httpRequest.getParameter(TICKET_PARAM_NAME);
		if (ticket != null) {
			String username;
			try {
				username = this.validateServerTicket(ticket);
			} catch (ValidateServerTicketException e) {
				httpResponse.sendRedirect(buildSigninUrl());
				return;
			}
			HttpSession requestSession = httpRequest.getSession();
			requestSession.setAttribute(IS_LOGINED, true);
			final String sessionId = requestSession.getId();

			// put the ticket and session information into map.
			this.serverTicketToSessionIdMapping.put(ticket, sessionId);
			AuthenticationFilter.sessionIdToSessionMapping.put(sessionId,
					requestSession);

			// put ServerTicket into cookie.
			Cookie stCookie = new Cookie(SERVER_TICKET_NAME, ticket);
			stCookie.setMaxAge(-1);
			stCookie.setPath(this.removeDomain(this.serverUrl));
			httpResponse.addCookie(stCookie);

			// Set attributes for web to get the username
			// put username into cookie and session.
			requestSession.setAttribute(SESSION_ATTR_NAME_USERNAME, username);
			Cookie usernameCookie = new Cookie(
					PassportJsonConstants.KEY_USERNAME, username);
			usernameCookie.setMaxAge(-1);
			usernameCookie.setPath(this.removeDomain(serverUrl));
			httpResponse.addCookie(usernameCookie);

			chain.doFilter(httpRequest, httpResponse);
			return;
		} else {
			httpResponse.sendRedirect(buildSigninUrl());
			return;
		}
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		this.serverUrl = filterConfig.getInitParameter("ServerURL");
		if (!this.serverUrl.endsWith("/"))
			this.serverUrl += "/";
		this.clientSignInUrl = filterConfig
				.getInitParameter("GetSsoServerTicket");
		this.validateServerTicketUrl = filterConfig
				.getInitParameter("ValidateTicketUrl");
	}

	@Override
	public void destroy() {
	}

	public static Map<String, HttpSession> getSessionMap() {
		return sessionIdToSessionMapping;
	}

	private String buildSigninUrl() {
		return this.clientSignInUrl + "?" + PARAM_NAME_CLIENT_IDENTIFIER + "="
				+ this.serverUrl + "&" + PARAM_NAME_REQUEST_URL + "="
				+ this.requestUrl;
	}

	private String removeDomain(String url) {
		Pattern p = Pattern.compile("^(https?://)[^/]*");
		Matcher m = p.matcher(url);
		url = m.replaceFirst("");
		return (url.endsWith("/")) ? url.substring(0, url.length() - 1) : url;
	}

	private String getServerTicketFromCookie(HttpServletRequest request) {
		Cookie[] cookies = request.getCookies();
		if (cookies == null)
			return null;
		for (Cookie cookie : cookies) {
			if (cookie.getName().equals(SERVER_TICKET_NAME))
				return cookie.getValue();
		}
		return null;
	}

	private void removeServerTicketCookie(HttpServletRequest request,
			HttpServletResponse response) {
		Cookie[] cookies = request.getCookies();
		if (cookies == null)
			return;
		for (Cookie cookie : cookies) {
			if (cookie.getName().equals(SERVER_TICKET_NAME)) {
				cookie.setMaxAge(0);
				response.addCookie(cookie);
				return;
			}
		}
	}

	@SuppressWarnings("rawtypes")
	private String getParams(HttpServletRequest httpRequest)
			throws UnsupportedEncodingException {
		String returnStr = "";
		Map properties = httpRequest.getParameterMap();
		Iterator entries = properties.entrySet().iterator();
		Map.Entry entry;
		while (entries.hasNext()) {
			String name = "";
			String value = "";
			entry = (Map.Entry) entries.next();
			name = (String) entry.getKey();
			Object valueObj = entry.getValue();
			if (valueObj == null) {
			} else if (valueObj instanceof String[]) {
				String[] values = (String[]) valueObj;
				for (int i = 0; i < values.length; i++) {
					value += values[i] + ",";
				}
				value = value.substring(0, value.length() - 1);
			} else {
				value = valueObj.toString();
			}
			returnStr += "&" + name + "=" + value;
		}
		if (!returnStr.equals(""))
			returnStr = returnStr.substring(1);

		return returnStr;
	}

	/**
	 * @return the username of the ST presented.
	 * @throws ValidateServerTicketException
	 *             if the server returns <code>null</code> value, no matter the
	 *             null return value or null username, this exception will be
	 *             thrown.
	 */
	private String validateServerTicket(String serverTicketValue)
			throws IOException, ValidateServerTicketException {
		HttpURLConnection urlConnection = (HttpURLConnection) new URL(
				this.validateServerTicketUrl).openConnection();
		urlConnection.setRequestMethod("POST");
		urlConnection.setDoOutput(true);
		urlConnection.setDoInput(true);
		urlConnection.setUseCaches(false);

		String params = this.buildPrarm(serverTicketValue);

		OutputStream os = urlConnection.getOutputStream();
		os.write(params.toString().getBytes());
		os.flush();
		os.close();

		BufferedReader br = new BufferedReader(new InputStreamReader(
				urlConnection.getInputStream()));
		String responseResult = br.readLine();
		String formattedResult = new String(responseResult.toString()
				.getBytes(), Charset.forName("UTF-8"));
		br.close();
		return this.getUsername(formattedResult);
	}

	private String buildPrarm(String serverTicketValue) {
		JSONObject requestJSON = new JSONObject();
		requestJSON.put(PassportJsonConstants.KEY_TICKET, serverTicketValue);
		requestJSON.put(PassportJsonConstants.KEY_CLIENT_IDENTIFIER,
				this.serverUrl);
		return PARAM_NAME_VALIDATE_REQUEST + "=" + requestJSON.toString();
	}

	private String getUsername(String formattedResult)
			throws ValidateServerTicketException {
		JSONObject resultJSON = JSONUtils.mayBeJSON(formattedResult) ? JSONObject
				.fromObject(formattedResult) : null;
		if (resultJSON == null
				|| !(Boolean) resultJSON.get(PassportJsonConstants.KEY_RESULT)) {
			throw new ValidateServerTicketException();
		}
		Object username = resultJSON.get(PassportJsonConstants.KEY_USERNAME);
		if (username == null)
			throw new ValidateServerTicketException();
		return (String) username;
	}

	// ------------ Getters and Setters ------------ //
	public String getServerUrl() {
		return serverUrl;
	}

	public void setServerUrl(String serverUrl) {
		this.serverUrl = serverUrl;
	}

	public String getSsoServerSignInUrl() {
		return clientSignInUrl;
	}

	public void setSsoServerSignInUrl(String ssoServerSignInUrl) {
		this.clientSignInUrl = ssoServerSignInUrl;
	}

	public String getValidateServerTicketUrl() {
		return validateServerTicketUrl;
	}

	public void setValidateServerTicketUrl(String validateTicketUrl) {
		this.validateServerTicketUrl = validateTicketUrl;
	}

	public String getRequestUrl() {
		return requestUrl;
	}

	public void setRequestUrl(String requestUrl) {
		this.requestUrl = requestUrl;
	}
}
